﻿//
// Copyright 2008-2011 by Burt Harris
// Licensed under the Microsoft Public License (Ms-PL)
// See: http://www.codeplex.com/yaml/license
//

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Runtime.Serialization;

namespace YaTools.Yaml
{
    public class TaggedObjectSink : Compose
    {
        private static void RegisterObject(Load load, object obj)
        {
            Contract.Requires(load != null);
            // This case is simple since the objects TaggedMapping's identity 
            // doesn't change at EndCollection time, so we can register
            // the object early enough we don't need fixups...
            // It won't always be so easy.
            if (!string.IsNullOrEmpty(load.Anchor))
            {
                Load e = load;
                e.AnchorObjectManager.RegisterObject(obj, e.Anchor);
            }
        }

        public override Compose BeginMappingNode(Load load)
        {
            if (load.Tag == Tag.NonSpecific) load.Tag = Tag.Map;

            load.Object = load.Tag == Tag.Map ? new Hashtable() : new TaggedDictionary(load.Tag);

            RegisterObject(load, load.Object);
            return this;
        }

        public override object ComposeMappingNode(Load load)
        {
            return load.Object;
        }

        public override Compose BeginSequenceNode(Load load)
        {
            if (load.Tag == Tag.NonSpecific) load.Tag = Tag.Sequence;
            load.Object = load.Tag == Tag.Sequence ? new ArrayList() : new TaggedSequence(load.Tag);
            RegisterObject(load, load.Object);
            return this;
        }

        public override object ComposeSequenceNode(Load load)
        {
            return load.Object;
        }
        
        public override object AddNullNode(Load load)
        {
            return null;
        }

        public override object AddScalarNode(Load load, string value)
        {
            object result;

            // TODO: should the conditions below include Tag.NonSpecificNonPlainScalar?

            if (load.Tag == Tag.NonSpecific || load.Tag == Tag.String)
                result = value;
            else
                result = new TaggedScalar(load.Tag, value);

            RegisterObject(load, result);
            return result;
        }

        public override object AddAliasNode(Load load, string anchor)
        {
            Load e = load;
            object result = e.AnchorObjectManager.GetObject(anchor);
            return result;
        }

        private static object CreatePair(object key, object value)
        {
            return new KeyValuePair<object, object>(key, value);
        }

        public override bool BeginObject(Load load, object value)
        {
            if (load.PreviousWasKey && load.IsKey)
            {
                // Two keys in a row: null value!
                if (load.Kind == Kind.Mapping)
                {
                    AddToIDictionary(load, null);
                }
                else if (load.Kind == Kind.Sequence)
                {
                    AddToIList(load, CreatePair(load.Key, null));
                }
            }
            return true;
        }

        public override void EndObject(Load load, object value)
        {
            switch (load.Kind)
            {
                case Kind.Mapping:
                    if (load.IsKey)
                    {
                        load.Key = value;
                    }
                    else if (load.PreviousWasKey)
                    {
                        AddToIDictionary(load, value);
                    }
                    else
                    {
                        throw new InvalidOperationException("Unexpected mapping sequence");
                    }
                    break;

                case Kind.Sequence:
                    if (load.IsKey) load.Key = value;
                    else if (load.PreviousWasKey)
                    {
                        AddToIList(load, CreatePair(load.Key, value));
                    }
                    else
                    {
                        AddToIList(load, value);
                    }
                    break;

                case Kind.Document:
                    load.Object = value;
                    break;
            }
        }

        private static void AddToIList(Load load, object value)
        {
            Contract.Requires(load != null);
            try
            {
                var list = (IList) load.Object;
                Contract.Assume(list != null);
                list.Add(value);
            }
            catch (ArgumentException ex)
            {
                throw new SerializationException(ex.Message);
            }
        }

        private static void AddToIDictionary(Load load, object value)
        {
            Contract.Requires(load != null);
            Contract.Assume(load.Key != null);

            var mapping = (IDictionary) load.Object;
            Contract.Assume(mapping != null);

            try
            {
                mapping.Add(load.Key, value);
            }
            catch (ArgumentException ex)
            {
                throw new SerializationException(ex.Message, ex);
            }
        }

        public override Compose BeginDocument(Load load)
        {
            return this;
        }

        public override object ComposeDocument(Load load)
        {
            return null;
        }
    }
}